穷究链表(一)


      今天上午申请了一个博客,晚上回来就开通了,博客园的效率真是让人赞赏。已经有计划希望将链表这个玩意完全的深挖一下,并以此为基础,来讨论一些C/C++语言特性使用,并讨论一些其他程序开发中的问题,刚好这两天大家放假都在开心呢,我就偷偷上几篇也省得被扔很多砖头。毕竟第一次写这样的文章,肯定里面有些没有考虑缜密的地方,同时本身也就是希望通过大家探讨来指导我的。希望能够通过不断的讨论,达到学习、进步的目的。

      作为程序员,链表这种数据结构应该是耳熟能详的了。没吃过猪肉还见过猪跑呢,大风大浪的哈希,图啊啥的都常用,何况这种基础的数据结构呢。不管是在学校里面学习数据结构的时候,在应聘考试笔试面试的时候,在公司工作应用各种库的时候,我相信大家应该或多或少都接触过链表。但是,真正的用某种特定的语言来实现链表的某些特定操作的时候,并不一定能够实现正确。这里,主要是讨论利用C和C++来实现一个链表的时候,遇到的各种问题。最终得到一个比较好的可以用于平时面试笔试等的程序。同时,也更好的理解好链表,达到“穷究”的目的。

      这里可能大家有疑问,为什么不是说可以应用的工程性的程序,而只是可以用于笔试和面试呢?对于C来说,比较好的用于工程性的程序应该是在linux的内核源码中;对于C++来说,其原生的STL中就有list可以来使用,并不需要我们来重新造轮子。而真正链表用于工程中,主要的就是一些异常的处理需要被考虑到,同时,一般就是使用双向链表来实现所有的链表操作。但是我们现在讨论链表,主要在于对链表本身的一些操作的理解和实现,如果将很多精力放在工程性的方面,那势必就会削弱本身强调的内容。同时,作为工程应用,如C中就通常又分为list.c和list.h来做,而C++中更是将其细分为很多模块(参考STL),有专门负责内存分配的allocator,有list,有iterator,还有各种模板函数,会搞得越来越复杂化。因此,我们的目的就是达到面试和笔试可以通过的程度,而这些都可以通过的话,恩,除非你就是去做库的,否则日常的工作是不会难倒你的。
      
      这里通过用C和C++分别来实现链表程序的过程,以及在实现过程中的思考来进行叙述。这里我使用的IDE环境为Visual studio 2005,如果你希望和我一起做的话,我推荐最好是这个版本或者以上的版本如2008。当然,2003或许也可以,但是我没有试验过,所以最好你能够自己来试验。文章中所有的完整代码均可以编译运行,也有一些标注为代码片段的函数,单个是不能编译的(当然,这个大家都清楚,但是,原谅我再啰嗦一下 :-P),代码片段是用来进行一些解释性工作的。当然,这些代码中有可能会有一些bug,希望大家能够帮助我找出它们,这样最终我们就会得到一个强壮的、完整的链表程序。
    
      Visual Studio是我使用过的最好用的IDE环境了,除了它的速度和为了安全性常报的一些warning,以及偶尔会出现的ICE之外,还是挺满意的。其实很多人,包括我,都是挺喜欢VC6的,但是其对模板的支持实在是让人欲哭无泪,加上VS实在是很好用,所以还是选择VS2005作为开发环境。
    
      这里首先会使用C语言来进行实现,然后对于C++,会先使用非模板的写法来写一个实现,然后用模板的写法来实现,最后是真正现实应用中得到的源码的赏析,C语言本身库中并不支持链表等数据类型,所以选取的是linux内核源码中的list.h,同时也下载了<Data Structures and Algorithm Analysis in C>第二版的源码程序来进行参考;C++语言中,选择的是标准库(STL)中的list,但是标准库在不同厂商的实现也有不同,包括SGI STL,VC中的实现以及BCB中的实现,我选择的是JJHOU老师的注释版本,同时也在SGI STL网站上下载了最新的3.3版本的源码。当然,BOOST库中也有list,尽管也下载了,但是还没有时间去一一分析,所以暂时不涉及到BOOST。
    
      尽管我现在就想去实现链表,但是还是发现很多东西还没写完,其实这也是软件工程的一个体现的方面,我们在实现之前做的工作越多,我们最后实现需要的时间就越少,而编码工作在整个工程中的比重就会越小。
   
      那我们之前会做哪些工作呢?
   
      首先要了解的是我们这里实现的,和现实中的链表程序是不同的。我们要实现的是一个单向链表。链表根据存储的指针的不同,可以分为单向链表、双向链表,根据指针指向的不同,还可以分为循环链表和双向循环链表。在现实世界中,纯粹的链表程序,最复杂的也就是双向循环链表了,也就是linux内核中的实现,当然,再将其加上一些指针,将其加入哈希表、红黑树等结构,就是另外的事情了。同时,要理解的一件事情就是,存储的内容越多,并不一定会增加复杂性,有时反而会降低复杂性。比如最简单的,单向链表要得到一个节点的前向节点就很麻烦,但是双向链表要得到该节点的前向节点,简直太easy了。又或者JAVA的元数据,又或者string类的size,这些都是为了降低复杂性而增加的内容。
    
      对于双向链表,循环链表,有其自己的操作特性,并不一定就是,实现了单向链表,其他显而易见,而且这些年读书过来的经验,一般书上说显而易见,就一点都不显而易见,但是对于这两种链表类型,并不一定有实现来实现,主要精力还是用来实现单向链表。
    
      对于单向链表,其还有两种实现方式,是对于链表的第一个节点的处理不同而造成的。一种是第一个节点和其他节点一样,是带有具体数据的;第二种是第一个节点是不带有数据的,空链表是带有一个这样节点的链表。我们一般称第一种为不带头节点的单向链表,而第二种被称为带头节点的单向链表。
    
      在实际工程实现中,一般我们采取第二种实现方式,因为这样对于添加、删除等操作,我们就可以不用去考虑一些特殊情况,在实现代码的时候比较方便,而我们所需要付出的代价仅仅是一个链表节点空间的“浪费”。所以在工程中,一般会比较偏爱选择这种来进行实现。
    
      但是在我们的实现中,会选择第一种来进行实现,主要原因是第一种的实现会涵盖第二种的情况,而如果仅仅实现了第二种,在实现第一种的时候,还是需要去考虑不同的情况,所以我们来实现比较繁琐的一个。
    
      OK,那就确定下来,我们现在是使用C语言,在VS2005下,来开发一个不带头节点的链表程序。
    
      C语言,根据其历史,分为C89与C99,具体2005支持的,并没有详细去查找,应该是C99。如果对C语言的一些语法不熟悉的话,可以去下载相关的manual。同时书的话,也就是K&R的TCPL了(The C Programming Language)。再加上一些C FAQ,以及MSDN来进行相关的参考。
    
      这里除了C++模板之外,在实现时都会遇到一个问题,我们的链表中存储的数据到底是什么类型的?当然,我们可以在C中使用void *来指向具体的保存数据的内存,这样来达到一个通用的目的,也可以使用linux中包含链表节点结构这种hack的方法来实现。但是我们这里仅仅是为了讨论的方便,存储的数据的类型定义为int型。
    
      说到int类型,就要考虑一下,机器类型,操作系统,编译器等是否会对程序有影响。使用机器为普通PC,机器为IA32架构,操作系统为windows XP 32位版本,编译环境前面已经提过,为VS2005,并没有使用其他的平台SDK。这个在嵌入式开发中考虑得比较重要,尤其是在数据进行传输后,还有大小端、字节对齐等问题,当然,现在我们并不需要考虑这些。
    
      同时,要说明的就是,我们的程序是单线程的。C和C++从语言层面是不支持多线程的,而是操作系统本身来支持多线程,类UNIX系统使用POSIX来支持多线程,而windows本身也有对应的API支持多线程,同时现在也支持POSIX了。而JAVA语言本身从语言层面就支持多线程。引入多线程操作,就需要加入锁机制,临界区等,这又是其他应用范畴了。同时,多线程的调试比单线程要麻烦得多,从只是介绍数据结构的方面来说,我们并不需要如此复杂。但是在工程方面,世界本身就是复杂的,我们只能去适应这样的复杂性。
    
      使用C和C++来实现一遍,而不是只是分析源码,同时先C再C++的目的:

因为在链表的实现过程中,会使用到很多CC++的基础知识,而且应该还是比较核心,比较深入一些的知识,(当然,使用一般的C/C++知识也可以完成,但是在具体优秀代码的实现过程中,还是使用了很多深入的思想的)

首先使用C来实现,因为使用C++还会增加很多C++特有的语法特性,增加很多其他的考虑和麻烦。比如友元类,友元函数,嵌套类,构造函数,析构函数,模板使用时的问题,包括具体编译器对于模板的支持问题,具体操作时参数如何设置,是否使用引用,返回值如何设置,是否使用引用。内存分配的问题,异常处理等。

所以先使用C,来将链表相关的操作编写清楚后,再来用C++来实现,查漏补缺,将对应的概念来梳理清楚。达到至此之后,链表再无问题的目的。

最后有个麻烦的就是画图问题,最好的就是在分析链表的时候,有对应的分析图,这样很直观明了,但是在纸上画好画,在电脑上画这样的图还是不好用,不知道有没有什么好的方法来描述数据结构的。

做这个的目的有两个:

一个是对链表本身的概念深入和程序熟悉,达到对链表本身的问题熟练解决的目的。
第二个是通过对链表的深入,来将各方面的知识点融汇贯通,达到提高自己技术的目的。


      其实经常会有一些错误的观点,认为自己写出的东西有这样那样的问题,而开源的代码,或者有名的代码应该就会很好,但是实际上,并不一定是如此,其基本的概念还是一样的,常常写东西的时候,有个其他代码的参考就感觉比较好,即使自己不太看,就感觉还不错,如果自己写的话,就会感觉无从下手,或者脑中一堆东西,很乱很复杂,这个还是编程太少的原因,需要慢慢改进,但是有意识的提高还是比无意识的锻炼要效果好得多的。

同时,在编写链表的时候,可以慢慢考虑,由简单的链表,如何保留接口,进行相应的扩展,合适各种需求的使用。


发现第一篇已经写了不少字数了,那关于单向链表的实现,就放在第二章开始。恩,大家如果不耐烦,想丢鸡蛋的话就丢吧,我闪~~

posted on 2009-10-02 22:07  cnyao  阅读(742)  评论(5编辑  收藏  举报